/*
 * ============================================================================
 *                   GNU Lesser General Public License
 * ============================================================================
 *
 *
 *
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307, USA.
 *
 *
 *
 */
package rad.framework.search.service;

import java.io.IOException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;

import rad.framework.search.directory.ManagedDirectory;
import rad.framework.trace.Trace;

import org.apache.commons.lang.StringUtils;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.index.CorruptIndexException;
import org.apache.lucene.queryParser.MultiFieldQueryParser;
import org.apache.lucene.queryParser.ParseException;
import org.apache.lucene.queryParser.QueryParser;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.ScoreDoc;
import org.jboss.seam.ScopeType;
import org.jboss.seam.annotations.Logger;
import org.jboss.seam.annotations.Name;
import org.jboss.seam.annotations.Scope;
import org.jboss.seam.log.Log;


/**
 * Creates and executes the search query.
 *
 * @author boudyacho
 */
@Name("searchAction")
@Scope(ScopeType.SESSION)
@Trace
public class SearchAction implements Serializable {
	private String searchPattern;
	private String classInError;
	
	private int currentPage = 1;
	private int pageSize = 5;
	private int totalPages = 1;
	
	//if the number of results is negative it indicates an error in user configuration of search.
	//-1: The POJOs aren't setup up correctly or haven't been reindexed since they've been fixed
	private int numberOfResults;
	List<SearchResult> searchResults;
	
	private static final int MAX_LENGTH = 500;
	
	@Logger
	private Log log;
	
	/**
	 * @return the total number of results the search query found. 
	 * */
	public int getNumberOfResults(){
		return numberOfResults;
	}
	
	/**
	 * @return the search terms
	 * */
	public String getSearchPattern() {
		return searchPattern;
	}

	/**
	 * @param searchPattern the search terms the user wants to query the database about.
	 * */
	public void setSearchPattern(String searchPattern) {
		this.searchPattern = searchPattern;
	}
	
	/**
	 * @return the page that is currently active.
	 * */
	public int getCurrentPage() {
		return currentPage;
	}

	/**
	 * @param currentPage the page that is currently active.
	 * */
	public void setCurrentPage(int currentPage) {
		this.currentPage = currentPage;
	}
	
	/**
	 * @return a list of searchResults
	 * */
	public List<SearchResult> getSearchResults() {
		return searchResults;
	}
	
	/**
	 * @return the number of results to be displayed on the page
	 * */
	public int getPageSize() {
		return pageSize;
	}

	/**
	 * @param pageSize the number of results to be displayed on the page
	 * */
	public void setPageSize(int pageSize) {
		this.pageSize = pageSize;
	}
	
	/**
	 * @return if the user incorrectly annotates their entities, this gives
	 * the class that was processing when the error occured.
	 * */
	public String getClassInError() {
		return classInError;
	}

	/**
	 * this is the function that gets called by the website. It starts the search process.
	 * @return "search"
	 * */
	public String doSearch() {
		if (!StringUtils.isEmpty(searchPattern)) {
			createAndExecuteQuery();
		}
		return "search";
	}
	
	/**
	 * Creates the lucene query from the searchPattern and the list of fields gathered from
	 * SearchMetaData. Queries the db, gathers the results, and calculates the total number
	 * of pages.
	 * */
	private void createAndExecuteQuery(){
		Query luceneQuery;
		searchResults = new ArrayList<SearchResult>();
		//populate the field set with all the entities fields
		Set<String> fields = SearchMetaData.instance().getFields();

		//setting up query and parser
		QueryParser parser = new MultiFieldQueryParser(fields.toArray(new String[fields.size()]), 
				new StandardAnalyzer());
		
		try {
			luceneQuery = parser.parse(searchPattern);
		} catch (ParseException pe) {
			log.error("parse error with: " +searchPattern);
			throw new RuntimeException(pe);
		}
		
		IndexSearcher isearcher = null;
		
		try {
			isearcher = new IndexSearcher(ManagedDirectory.instance());
			ScoreDoc[] hits = isearcher.search(luceneQuery, null, 1000).scoreDocs;
			numberOfResults = hits.length;
			
			//figure out the total number of pages
			double dblNumResults = numberOfResults;
			double dblPageSize = pageSize;
			totalPages = (int) Math.ceil(dblNumResults / dblPageSize);
			
			//setup searchResults for the relevant hits
			parseDocuments(isearcher, hits);			
			isearcher.close();
		} catch (CorruptIndexException e) {
			log.error("The Index: " +" is corrupted.");
			throw new RuntimeException(e);
		} catch (IOException e) {
			log.error("Input/Output Error");
			throw new RuntimeException(e);
		}
	}
	
	/**
	 * Creates the correct number of SearchResult objects for the current
	 * page. 
	 * 
	 * @param is the indexSearcher pointed to the directory where indices are stored.
	 * @param hits the results from the query.
	 * */
	private void parseDocuments(IndexSearcher is, ScoreDoc[] hits){
		//initializing search results variable...
		boolean idCheck = false, titleCheck = false, descriptionCheck = false;
		String resultClass = null, resultId = null, resultTitle = null, resultDescription = null;
		
		//loops through the results and creates a SearchResult for each entry
		int resultSize;
		int num = ((currentPage-1)*pageSize)+1;
		
		//where to results start
		int offset = (currentPage-1) * pageSize;
		//if there are less than pageSize results left set result size to the # left.
		if(pageSize > (hits.length - offset)){
			resultSize = (offset > hits.length) ? 0 : hits.length - offset;
		}else{
			resultSize = pageSize;
		}
		
		for(int i=0;i<resultSize;i++){
			try{
				//get the relevant document
				Document docu = is.doc(hits[i+offset].doc);
				
				//reset the error correction variables
				idCheck = titleCheck = descriptionCheck = false;

				//extract the class name
				resultClass = docu.getField("_hibernate_class").stringValue();
				int lastDot = resultClass.lastIndexOf(".") + 1;
				resultClass = resultClass.substring(lastDot);

				//extract the class id
				resultId = docu.getField("id").stringValue();
				idCheck=true;

				//extract the title
				resultTitle = docu.getField("title").stringValue();
				titleCheck=true;

				//extract the description
				resultDescription = docu.getField("description").stringValue();
				descriptionCheck=true;
				resultDescription = truncateStringOnWordBoundary(resultDescription, MAX_LENGTH);

				//construct search result and append to list
				searchResults.add(new SearchResult(("/jsf/"+resultClass+"/View.seam"), 
						resultTitle, resultDescription, resultId, num));
			}catch(NullPointerException npe){
				/*if the id or title aren't available the user needs to check his/her
				annotations and reindex the entities.*/
				if(idCheck == false || titleCheck == false){
					classInError = resultClass;
					numberOfResults = -1;
					return;
					/*if the description isn't available (an object might only have one property)
					use the title as a description.*/
				}else if(descriptionCheck == false){
					searchResults.add(new SearchResult(("/jsf/"+resultClass+"/View.seam"), 
							resultTitle, resultTitle, resultId, num));
				}
			} catch (CorruptIndexException e) {
				log.error("The Index: " + ManagedDirectory.instance().toString()+ " is corrupted.");
				throw new RuntimeException(e);
			} catch (IOException e) {
				log.error("Input/Output Error");
				throw new RuntimeException(e);
			}
			num++;
		}
	}
	
    protected String truncateStringOnWordBoundary(String string, int length) {
        if (string.length() <= length) return string;

        char [] chars = string.toCharArray();
        StringBuffer buffer = new StringBuffer();
        String result = "";
        int lastWhitespace = 0;
        for (int i = 0; i < chars.length; i++) {
            if (chars[i] == ' ') lastWhitespace = i;
            buffer.append(chars[i]);

            if (i >= length) {
                result = buffer.substring(0, lastWhitespace);
                break;
            }
        }
        return result + " ...";
    }
	
	/*
	 * Creates a list of SearchPage objects formated to display google-like
	 * pagination for search result links.
	 * */
	/**
	 * Returns a list of SearchPage objects to navigate through the various pages.
	 * 
	 * @return list of SearchPage objects that represent the navigation for the pagination.
	 * */
	public List getPages(){
		List<SearchPage> pages = new ArrayList<SearchPage>();
		boolean dots = true;

		//is there only one page of results?
		if(totalPages == 1 || totalPages == 0 || currentPage > totalPages){
			return pages;
		}

		//should I include a previous link?
		if(currentPage > 1){
			pages.add(new SearchPage("Prev ",(currentPage-1)));
		}

		//add the first page link
		if(currentPage == 1){
			pages.add(new SearchPage("[1]",1));
		}else{
			pages.add(new SearchPage(1));
		}

		//print out previous pages
		if(currentPage > 2){
			if((currentPage == totalPages) && (totalPages > 3)){
				pages.add(new SearchPage("..."+(currentPage-2),(currentPage-2)));
				dots=false;
			}
			if(dots){
				pages.add(new SearchPage("..."+(currentPage-1),(currentPage-1)));
			}else{
				pages.add(new SearchPage(currentPage-1));
			}
		}

		//add the current page to the list
		if(currentPage != 1 && currentPage != totalPages){
			pages.add(new SearchPage("["+currentPage+"]",currentPage));
		}

		//if there is more than one page left
		if(currentPage < (totalPages - 1)){
			if(currentPage == 1 && totalPages > 3){
				pages.add(new SearchPage((currentPage+1)));

				if(currentPage < (totalPages-2)){
					pages.add(new SearchPage((currentPage+2)+"...",(currentPage+2)));
				}else{
					pages.add(new SearchPage(currentPage+2));
				}
			}else{
				//removes the dots if last page is in range
				if(currentPage < (totalPages-2)){
					pages.add(new SearchPage((currentPage+1)+"...",(currentPage+1)));
				}else{
					pages.add(new SearchPage(currentPage+1));
				}
			}
		}

		//add the last page link
		if(currentPage == totalPages){
			pages.add(new SearchPage("["+totalPages+"]",totalPages));
		}else{
			pages.add(new SearchPage(totalPages));
		}

		//should i include a next link?
		if(currentPage < totalPages){
			pages.add(new SearchPage("Next ",(currentPage+1)));
		}

		return pages;
	}
}